import { useElementSize } from '@vueuse/core'
import {
  toRef,
  type ComputedRef,
  type Ref,
  reactive,
  watch,
  ref,
  readonly,
  type DeepReadonly,
  type UnwrapNestedRefs
} from 'vue'
import { useSheet, type CSSPropertiesRuleValue } from './useSheet'

type ToRefArg<T> = T | Ref<T> | ComputedRef<T> | (() => T)

type Shim<T extends {}> = T & { [K in keyof T as K extends string ? `${K}Shim` : never]-?: T[K] }

export enum StickyType {
  /** 动态节点 */
  RESIZE,
  /** 静态节点 */
  STATIC,
  /** 后代节点 */
  POSTERITY
}

export interface ResizeStickyElement<K extends string> {
  key: K
  type: StickyType.RESIZE
  zIndex?: CSSPropertiesRuleValue<number>
}

export interface StaticStickyElement<K extends string> {
  key: K
  type: StickyType.STATIC
  height: number | string
  zIndex?: CSSPropertiesRuleValue<number>
}

export interface PosterityStickyElement<K extends string> {
  key: K
  type: StickyType.POSTERITY
  height: number | string
  selector: string | string[]
  zIndex?: CSSPropertiesRuleValue<number>
}

export type StickyElement<K extends string> =
  | ResizeStickyElement<K>
  | StaticStickyElement<K>
  | PosterityStickyElement<K>

export interface StickyOption {
  /** 公共的偏移量 */
  offset?: number | string
  /** 控制是否生效 */
  efficient?: ToRefArg<boolean>
  /** 公共的 z-index 配置 */
  zIndex?: CSSPropertiesRuleValue<number>
}

export interface StickyInfo {
  class: any
  ref?: any
}

let id = 1

function superposition() {
  let num = 0
  const str: string[] = []

  function entering(value?: string | number) {
    switch (typeof value) {
      case 'number': {
        num += value
        break
      }
      case 'string': {
        value.length && str.push(value)
        break
      }
      default:
        break
    }
  }

  function compute() {
    const data = [...str]
    num && data.push(`${num}px`)
    switch (data.length) {
      case 0:
        return '0px'
      case 1:
        return data[0]
      default:
        return `calc(${data.join(' + ')})`
    }
  }

  return { entering, compute }
}

export function useSticky<
  Keys extends string,
  Infos extends { [K in Keys]?: StickyInfo } = { [K in Keys]?: StickyInfo }
>(items: ToRefArg<StickyElement<Keys>[]>, option: StickyOption = {}) {
  const { zIndex, offset, efficient = true } = option

  const { setRule, removeRule } = useSheet()

  const stickyItems = toRef(items) as Ref<StickyElement<Keys>[]>
  const stickyEfficient = toRef(efficient)
  const stickyInfos = reactive({} as Infos)

  const heightList: (number | string)[] = []
  const classNameList: string[] = []
  const selectorTextList: string[] = []

  function clear() {
    selectorTextList.forEach(removeRule)

    heightList.length = 0
    classNameList.length = 0
  }

  function clearInfos() {
    const keys = stickyItems.value.map(({ key }) => key) as string[]
    for (const key in stickyInfos) {
      if (!keys.includes(key)) {
        delete stickyInfos[key]
      }
    }
  }

  function init() {
    clear()
    clearInfos()

    stickyItems.value.forEach((item, index) => {
      heightList[index] = 0
      classNameList[index] = `sticky-superposition-${id++}`
      selectorTextList[index] = `.${classNameList[index]}`

      const info: StickyInfo = { class: { [classNameList[index]]: stickyEfficient } }

      switch (item.type) {
        case StickyType.RESIZE: {
          const target = ref()

          watch(useElementSize(target).height, (height) => {
            heightList[index] = height
            updateStyleRule()
          })

          info.ref = (el: any) => {
            target.value = el
          }
          break
        }
        case StickyType.STATIC: {
          heightList[index] = item.height
          break
        }
        case StickyType.POSTERITY: {
          heightList[index] = item.height
          let { selector } = item
          if (!Array.isArray(selector)) selector = [selector]
          selector = selector.filter(Boolean)
          if (!selector.length) throw new Error('请输入有效的后代节点选择器')
          selectorTextList[index] = selector.map((sel) => `.${classNameList[index]} ${sel}`).join(', ')
          break
        }
      }

      // @ts-ignore
      stickyInfos[item.key] = info
    })

    updateStyleRule()
  }

  function updateStyleRule() {
    const { entering, compute } = superposition()
    entering(offset)

    stickyItems.value.forEach((item, index) => {
      const selectorText = selectorTextList[index]

      setRule(selectorText, {
        position: 'sticky',
        top: compute(),
        zIndex: item.zIndex || zIndex
      })

      entering(heightList[index])
    })
  }

  watch(stickyItems, init, { immediate: true, deep: true })

  const shim = readonly({})
  const shimRegExp = /Shim$/

  return {
    stickyItems,
    stickyEfficient,
    stickyInfos: new Proxy(stickyInfos, {
      get(target, prop, receiver) {
        return (
          Reflect.get(target, prop, receiver) ??
          Reflect.get(target, (prop as string)?.replace(shimRegExp, ''), receiver) ??
          shim
        )
      },
      set() {
        return true
      },
      has(target, prop) {
        return Reflect.has(target, prop) || shimRegExp.test(prop as string)
      }
    }) as DeepReadonly<Shim<UnwrapNestedRefs<Infos>>>
  }
}
